/** * UPnP PortMapper - A tool for managing port forwardings via UPnP * Copyright (C) 2015 Christoph Pirkl <christoph at users.sourceforge.net> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package org.chris.portmapper.gui; import java.awt.Component; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.KeyEvent; import java.beans.PropertyChangeSupport; import java.util.Arrays; import java.util.LinkedList; import java.util.List; import javax.swing.ActionMap; import javax.swing.BorderFactory; import javax.swing.DefaultCellEditor; import javax.swing.JButton; import javax.swing.JCheckBox; import javax.swing.JComboBox; import javax.swing.JComponent; import javax.swing.JDialog; import javax.swing.JLabel; import javax.swing.JOptionPane; import javax.swing.JPanel; import javax.swing.JScrollPane; import javax.swing.JTable; import javax.swing.JTextField; import javax.swing.KeyStroke; import javax.swing.ListSelectionModel; import javax.swing.event.ListSelectionEvent; import javax.swing.event.ListSelectionListener; import net.miginfocom.swing.MigLayout; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.chris.portmapper.PortMapperApp; import org.chris.portmapper.Settings; import org.chris.portmapper.model.PortMapping; import org.chris.portmapper.model.PortMappingPreset; import org.chris.portmapper.model.Protocol; import org.chris.portmapper.model.SinglePortMapping; import org.jdesktop.application.Action; import org.jdesktop.application.ResourceMap; /** * This class represents the edit preset dialog. */ public class EditPresetDialog extends JDialog { private static final long serialVersionUID = 1L; public static final String PROPERTY_PORTS = "ports"; private JTextField remoteHostTextField, internalClientTextField, presetNameTextField; private final List<SinglePortMapping> ports; private final PropertyChangeSupport propertyChangeSupport; private JCheckBox useLocalhostCheckBox; private JTable portsTable; private final static String DIALOG_NAME = "preset_dialog"; private final static String ACTION_SAVE = DIALOG_NAME + ".save"; private final static String ACTION_CANCEL = DIALOG_NAME + ".cancel"; private static final String ACTION_ADD_PORT = DIALOG_NAME + ".add_port"; private static final String ACTION_ADD_PORT_RANGE = DIALOG_NAME + ".add_port_range"; private static final String ACTION_REMOVE_PORT = DIALOG_NAME + ".remove_port"; private static final String PROPERTY_PORT_SELECTED = "portSelected"; private final Logger logger = LoggerFactory.getLogger(this.getClass()); private final PortMappingPreset editedPreset; private PortsTableModel tableModel; private final PortMapperApp app; public EditPresetDialog(final PortMapperApp app, final PortMappingPreset portMappingPreset) { super(app.getMainFrame(), true); this.app = app; this.editedPreset = portMappingPreset; this.ports = new LinkedList<>(); this.setName(DIALOG_NAME); initComponents(); copyValuesFromPreset(); this.propertyChangeSupport = new PropertyChangeSupport(ports); propertyChangeSupport.addPropertyChangeListener(PROPERTY_PORTS, tableModel); // Register an action listener that closes the window when the ESC // button is pressed final KeyStroke escKeyStroke = KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0, true); final ActionListener windowCloseActionListener = new ActionListener() { @Override public final void actionPerformed(final ActionEvent e) { cancel(); } }; getRootPane().registerKeyboardAction(windowCloseActionListener, escKeyStroke, JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT); } private void copyValuesFromPreset() { remoteHostTextField.setText(editedPreset.getRemoteHost()); presetNameTextField.setText(editedPreset.getDescription()); for (final SinglePortMapping port : editedPreset.getPorts()) { this.ports.add((SinglePortMapping) port.clone()); } final boolean useLocalhost = (editedPreset.getInternalClient() == null); final String localhostAddress = app.getLocalHostAddress(); if (useLocalhost && localhostAddress == null) { useLocalhostCheckBox.setSelected(false); useLocalhostCheckBox.setEnabled(false); } else { useLocalhostCheckBox.setSelected(useLocalhost); internalClientTextField.setEnabled(!useLocalhost); internalClientTextField.setText(useLocalhost ? localhostAddress : editedPreset.getInternalClient()); } } private static JLabel createLabel(final String name) { final JLabel newLabel = new JLabel(name); newLabel.setName(name); return newLabel; } private void initComponents() { final ActionMap actionMap = app.getContext().getActionMap(this.getClass(), this); final JPanel dialogPane = new JPanel(new MigLayout("", // Layout // Constraints "[right]rel[left,grow 100]", // Column Constraints "")); // Row Constraints presetNameTextField = new JTextField(); dialogPane.add(createLabel("preset_dialog.description"), "align label"); dialogPane.add(presetNameTextField, "span 2, growx, wrap"); remoteHostTextField = new JTextField(); remoteHostTextField.setColumns(10); dialogPane.add(createLabel("preset_dialog.remote_host"), ""); dialogPane.add(remoteHostTextField, "growx"); dialogPane.add(createLabel("preset_dialog.remote_host_empty_for_all"), "wrap"); //$NON-NLS-2$ internalClientTextField = new JTextField(); internalClientTextField.setColumns(10); useLocalhostCheckBox = new JCheckBox("preset_dialog.internal_client_use_local_host", true); useLocalhostCheckBox.setName("preset_dialog.internal_client_use_local_host"); useLocalhostCheckBox.addActionListener(new ActionListener() { @Override public void actionPerformed(final ActionEvent arg0) { internalClientTextField.setEnabled(!useLocalhostCheckBox.isSelected()); if (useLocalhostCheckBox.isSelected()) { internalClientTextField.setText(app.getLocalHostAddress()); } else { // internalClientTextField.setText(""); } } }); // Check if the local host address can be retrieved final String localHostAddress = app.getLocalHostAddress(); if (localHostAddress != null) { logger.debug("Found localhost address " + localHostAddress + ". Enable localhost checkbox."); internalClientTextField.setText(localHostAddress); internalClientTextField.setEnabled(false); useLocalhostCheckBox.setEnabled(true); } else { logger.debug("Did not find localhost address: disable localhost checkbox."); useLocalhostCheckBox.setSelected(false); useLocalhostCheckBox.setEnabled(false); internalClientTextField.setEnabled(true); internalClientTextField.setText(""); } dialogPane.add(createLabel("preset_dialog.internal_client"), "align label"); dialogPane.add(internalClientTextField, "growx"); dialogPane.add(useLocalhostCheckBox, "wrap"); dialogPane.add(getPortsPanel(), "span 3, grow, wrap"); dialogPane.add(new JButton(actionMap.get(ACTION_CANCEL)), "tag cancel, span 2"); final JButton okButton = new JButton(actionMap.get(ACTION_SAVE)); dialogPane.add(okButton, "tag ok, wrap"); setContentPane(dialogPane); dialogPane.getRootPane().setDefaultButton(okButton); setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE); setModal(true); pack(); } private Component getPortsPanel() { final ActionMap actionMap = app.getContext().getActionMap(this.getClass(), this); final JPanel portsPanel = new JPanel(new MigLayout("", "", "")); portsPanel.setBorder( BorderFactory.createTitledBorder(app.getResourceMap().getString("preset_dialog.ports.title"))); tableModel = new PortsTableModel(app, ports); portsTable = new JTable(tableModel); portsTable.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION); portsTable.getSelectionModel().addListSelectionListener(new ListSelectionListener() { @Override public void valueChanged(final ListSelectionEvent e) { firePropertyChange(PROPERTY_PORT_SELECTED, false, isPortSelected()); } }); final JComboBox<Protocol> protocolComboBox = new JComboBox<>(); protocolComboBox.addItem(Protocol.TCP); protocolComboBox.addItem(Protocol.UDP); portsTable.getColumnModel().getColumn(0).setCellEditor(new DefaultCellEditor(protocolComboBox)); portsPanel.add(new JScrollPane(portsTable), "spany 3"); portsPanel.add(new JButton(actionMap.get(ACTION_ADD_PORT)), "wrap"); portsPanel.add(new JButton(actionMap.get(ACTION_ADD_PORT_RANGE)), "wrap"); portsPanel.add(new JButton(actionMap.get(ACTION_REMOVE_PORT)), "wrap"); return portsPanel; } protected void presetSelected(final PortMapping item) { this.presetNameTextField.setText(item.getDescription()); this.remoteHostTextField.setText(item.getRemoteHost()); // this.externalPortSpinner.setValue(item.getExternalPort()); // this.internalPortSpinner.setValue(item.getInternalPort()); if (item.getInternalClient() != null) { this.useLocalhostCheckBox.setSelected(false); this.internalClientTextField.setText(item.getInternalClient()); } } @Action(name = ACTION_ADD_PORT) public void addPort() { addPort(Protocol.TCP, 1, 1); } public void addPort(final Protocol protocol, final int internalPort, final int externalPort) { this.ports.add(new SinglePortMapping(protocol, internalPort, externalPort)); firePropertyChange(PROPERTY_PORT_SELECTED, false, isPortSelected()); propertyChangeSupport.firePropertyChange(PROPERTY_PORTS, null, this.ports); } @Action(name = ACTION_ADD_PORT_RANGE) public void addPortRange() { logger.debug("Open port range dialog"); app.show(new AddPortRangeDialog(app, this)); } @Action(name = ACTION_REMOVE_PORT, enabledProperty = PROPERTY_PORT_SELECTED) public void removePort() { // We have to delete the rows in descending order, else the wrong // row could be deleted. When removing a row, the indices of the // following rows will change. So we will delete the wrong row // when we try to delete a following row. // An IndexOutOfBoundsException could also occur. final int[] selectedRows = portsTable.getSelectedRows(); Arrays.sort(selectedRows); for (int i = selectedRows.length - 1; i >= 0; i--) { final int row = selectedRows[i]; logger.debug("Removing row " + row); this.ports.remove(row); } firePropertyChange(PROPERTY_PORT_SELECTED, false, isPortSelected()); propertyChangeSupport.firePropertyChange(PROPERTY_PORTS, null, this.ports); } public boolean isPortSelected() { return this.portsTable.getSelectedRowCount() > 0; } /** * This method is executed when the user clicks the save button. The method saves the entered preset */ @Action(name = ACTION_SAVE) public void save() { // Check, if the user entered a name for the preset and show an error // message. final String name = presetNameTextField.getText(); if (name == null || name.trim().isEmpty()) { showErrorMessage("preset_dialog.error.title", "preset_dialog.error.no_description"); return; } // Check, if a preset with the same name already exists. final Settings settings = app.getSettings(); for (final PortMappingPreset preset : settings.getPresets()) { if (preset != editedPreset && preset.getDescription() != null && preset.getDescription().equals(name)) { showErrorMessage("preset_dialog.error.title", "preset_dialog.error.duplicate_name"); return; } } // Check, if the user added at least one port mapping to the preset. if (tableModel.getRowCount() == 0) { showErrorMessage("preset_dialog.error.title", "preset_dialog.error.no_ports"); return; } if (useLocalhostCheckBox.isSelected()) { editedPreset.setInternalClient(null); } else { editedPreset.setInternalClient(internalClientTextField.getText()); } editedPreset.setRemoteHost(remoteHostTextField.getText()); editedPreset.setDescription(name); editedPreset.setPorts(this.ports); editedPreset.save(settings); logger.info("Saved preset '" + editedPreset.toString() + "'."); this.dispose(); } @Action(name = ACTION_CANCEL) public void cancel() { this.setVisible(false); this.dispose(); } private void showErrorMessage(final String titleKey, final String messageKey) { final ResourceMap resourceMap = app.getResourceMap(); JOptionPane.showMessageDialog(this, resourceMap.getString(messageKey), resourceMap.getString(titleKey), JOptionPane.ERROR_MESSAGE); } }